import tensorflow as tf
import numpy as np


# 定义CNN网络实现的类
class TextCNN(object):
    """
    A CNN for text classification.
    Uses an embedding layer, followed by a convolutional, max-pooling and softmax layer.
    嵌入层、卷积层、最大池化层、
    """
    def __init__(
      self, sequence_length, num_classes, vocab_size,
      embedding_size, filter_sizes, num_filters, l2_reg_lambda=0.0):  # 把train.py中TextCNN里定义的参数传进来
        """
        :param sequence_length:     句子的最大长度（词汇数）
        :param num_classes:         CNN最后一层分类的数目
        :param vocab_size:          词汇量的大小，总的词向量维度是 [vocabulary_size, embedding_size]
        :param embedding_size:      每个单词的词向量的长度
        :param filter_sizes:        卷积核尺寸，每个卷积核每次覆盖几个单词。对于每个卷积核，都有 num_filters。
        :param num_filters:         每个卷积核的数量
        :param l2_reg_lambda:       正则化次数
        """

        # Placeholders for input, output and dropout
        """
        input_x 输入语料,待训练的内容, 维度是sequence_length, "N个词构成的N维向量"；
        input_y 输入语料,待训练的内容标签, 维度是num_classes, "正面 || 负面"；
        dropout_keep_prob dropout参数,防止过拟合,训练时用；测试时禁止。
        tf.placeholder 创建一个占位符变量，在训练阶段或者测试阶段时，都可以使用它向我们的模型输入数据。
        第二个参数是输入张量的形状。None 的意思是该维度的长度可以是任何值。
        在我们的模型中，第一个维度是批处理大小，而使用 None 来表示这个值，说明网络允许处理任意大小的批次。
        在 dropout 层中，我们使用 dropout_keep_prob 参数来控制神经元的激活程度。
        """
        self.input_x = tf.placeholder(tf.int32, [None, sequence_length], name="input_x")
        self.input_y = tf.placeholder(tf.float32, [None, num_classes], name="input_y")
        self.dropout_keep_prob = tf.placeholder(tf.float32, name="dropout_keep_prob")

        # Keeping track of l2 regularization loss (optional)
        l2_loss = tf.constant(0.0)  # 先不用，写0

        # Embedding layer 第一层：嵌入层
        """
        作用：将词汇索引映射到低维度的词向量进行表示，本质是从数据中学习得到一个词汇向量表。   
        tf.device("/cpu:0") 强制代码在CPU上面执行操作，默认是GPU，嵌入层的操作目前还不支持GPU运行
        tf.name_scope 创建一个为"embedding"的新的名称范围，该范围将所有的操作都添加到这个"embedding"节点下面
        tf.random_uniform 随机产生均匀分布的矩阵，值的区间为[-1,1]
        tf.nn.embedding_lookup 选取一个张量里面索引对应的元素，创建实际的嵌入读取操作，
        这个嵌入操作返回的数据维度是三维张量 [None, sequence_length, embedding_size] 。
        因为TensorFlow 的卷积操作 conv2d 需要一个四维的输入数据，对应的维度分别是批处理大小，宽度，高度和通道数。
        在我们嵌入层得到的数据中不包含通道数，所以我们需要手动添加它，所以最终的数据维度是 [None, sequence_length, embedding_size, 1] 。
        """
        with tf.device('/cpu:0'), tf.name_scope("embedding"):  # 指定cpu
            self.W = tf.Variable(tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0), name="W")  # 词向量矩阵 初始化
            self.embedded_chars = tf.nn.embedding_lookup(self.W, self.input_x)  # 找到索引对应的词向量
            self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)  # 加一个维度，转换为4维的格式

        # Create a convolution + maxpool layer for each filter size
        """
        卷积层和池化层：
        我们使用的卷积核是不同尺寸的。因为每个卷积核经过卷积操作之后产生的张量是不同维度的，所以我们需要为每一个卷积核创建一层网络，
        最后再把这些卷积之后的结果合并成一个大的特征向量。
        代码中，W 表示不同的卷积核，h 表示对经过卷积得到的输出结果进行非线性处理之后的结果。卷积核：函数。
        每个卷积核会覆盖整个词向量长度，但是滑动覆盖几个单词就是不同的了。
        VALID 填充意味着，我们的卷积核只在我们的单词上面滑动，而不填充边缘，是执行窄卷积，所有最后输出的维度是 [1, sequence_length - filter_size + 1, 1, 1] 。
        对经过特定卷积的输出，我们做最大池化操作，使得我们得到的张量维度是 [batch_size, 1, 1, num_filters]。
        这实质上就是一个特征向量，其中最后一个维度就是对应于我们的特征。
        一旦我们拥有了来自各个卷积核的输出向量，那么我们就可以把它们合并成一个长的特征向量，该向量的维度是 [batch_size, num_filters_total] 。
        在 tf.reshape 中使用 -1，就是告诉 TensorFlow 在可能的情况下，将维度进行展平。
        """
        pooled_outputs = []
        for i, filter_size in enumerate(filter_sizes):  # filter_sizes卷积核尺寸,枚举后遍历
            with tf.name_scope("conv-maxpool-%s" % filter_size):
                # Convolution Layer 卷积层
                filter_shape = [filter_size, embedding_size, 1, num_filters]
                # 4个参数分别为filter_size高h，embedding_size宽w，channel为1，filter个数
                W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W")  # W进行高斯初始化
                b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name="b")  # b给初始化为一个常量
                conv = tf.nn.conv2d(
                    self.embedded_chars_expanded,
                    W,
                    strides=[1, 1, 1, 1],  # 步长
                    padding="VALID",
                    name="conv")
                # Apply nonlinearity 激活函数
                # 可以理解为,正面或者负面评价有一些标志词汇,这些词汇概率被增强，即一旦出现这些词汇,倾向性分类进正或负面评价,
                # 该激励函数可加快学习进度，增加稀疏性,因为让确定的事情更确定,噪声的影响就降到了最低。
                h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")

                # Maxpooling over the outputs 池化
                pooled = tf.nn.max_pool(
                    h,
                    ksize=[1, sequence_length - filter_size + 1, 1, 1],  # 过滤器大小
                    strides=[1, 1, 1, 1],  # 步长
                    padding='VALID',  # 这里不需要padding
                    name="pool")
                pooled_outputs.append(pooled)

        # Combine all the pooled features
        num_filters_total = num_filters * len(filter_sizes)  # 128x3
        self.h_pool = tf.concat(pooled_outputs, 3)  # 拼接
        self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])  # 扁平化数据，跟全连接层相连

        # Add dropout
        """
        Dropout层：
        Dropout 也许是最流行的方法来正则化卷积神经网络。
        Dropout 的思想非常简单，就是按照一定的概率来“禁用”一些神经元的发放。这种方法可以防止神经元共同适应一个特征，而迫使它们单独学习有用的特征。
        神经元激活的概率，我们从参数 dropout_keep_prob 中得到。我们在训练阶段将其设置为 0.5，在测试阶段将其设置为 1.0（即所有神经元都被激活）。
        drop层,防止过拟合,参数为dropout_keep_prob
        过拟合的本质是采样失真,噪声权重影响了判断，如果采样足够多,足够充分,噪声的影响可以被量化到趋近事实,也就无从过拟合。
        即数据越大,drop和正则化就越不需要。
        """
        with tf.name_scope("dropout"):
            self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)

        # Final (unnormalized) scores and predictions
        """
        分数和预测：
        我们使用来自池化层的特征向量（经过Dropout），然后通过全连接层，得到一个分数最高的类别。
        我们还可以应用softmax函数来将原始分数转换成归一化概率，但这个操作是保护会改变我们的最终预测。
        代码中，tf.nn.xw_plus_b 是一个很方便的函数，实现 Wx + b 操作。
        """
        # 输出层
        with tf.name_scope("output"):
            W = tf.get_variable(
                "W",
                shape=[num_filters_total, num_classes],  # 前面连扁平化后的池化操作
                initializer=tf.contrib.layers.xavier_initializer())  # 定义初始化方式
            b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name="b")

            # 损失函数导入
            l2_loss += tf.nn.l2_loss(W)
            l2_loss += tf.nn.l2_loss(b)

            # xw+b
            self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name="scores")  # 得分函数，每个类别的概率
            self.predictions = tf.argmax(self.scores, 1, name="predictions")  # 预测结果，返回最大的那个数值所在的下标

        # Calculate mean cross-entropy loss
        """
        损失函数和正确率：
        使用我们上面求得的分数，我们可以定义损失函数。损失值是对模型所造成的误差的度量，我们的目标是最小化这个损失值。
        分类问题的标准损失函数是交叉熵损失函数。
        这里，tf.nn.softmax_cross_entropy_with_logits 是一个方便的函数，用来计算每个类别的交叉损失熵，对于我们给定的分数和输入的正确标签。
        然后，我们计算损失值的平均值。当然，我们也可以对它们进行求和，但是这会对不同批大小的损失值衡量非常困难，尤其是在训练阶段和测试阶段。
        """
        with tf.name_scope("loss"):
            losses = tf.nn.softmax_cross_entropy_with_logits(logits=self.scores, labels=self.input_y)
            self.loss = tf.reduce_mean(losses) + l2_reg_lambda * l2_loss

        # Calculate Accuracy
        """
        定义了一个正确率的函数，它的作用就是在训练阶段和测试阶段来跟踪模型的性能。
        """
        # 准确率，求和计算算数平均值
        with tf.name_scope("accuracy"):
            correct_predictions = tf.equal(self.predictions, tf.argmax(self.input_y, 1))  # 返回值bool类型
            self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float"), name="accuracy")  # bool转成float
